/*
 * Copyright 2011 Luis Atencio
 * 
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.rt.io.file;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.Reader;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.rt.io.Utils;

/**
 * Provides a reading strategy for property files or 
 * files ending in .properties 
 * 
 * @author Luis Atencio
 * 
 */
public class PropertyFileStrategy extends AbstractFileStrategy {

	private static final char[] WHITE_SPACE = new char[]{' ', '\t', '\f'};
	
	private static final char[] SEPARATORS = new char[] {'=', ':'};

	public PropertyFileStrategy(String path, String fileName) {
		super(path, fileName);
	}
	
	@Override
	public Map<String, String> bindProperties() {

		URL url = Utils.locate(path, fileName);

		if (url == null) {
			throw new IllegalStateException("Cannot locate file with path: "
					+ path + " and file name " + fileName);
		}

		return load(url);
	}

	private Map<String, String> load(URL url) throws RuntimeException {

		// throw an exception if the target URL is a directory
		final Charset UTF8 = Charset.forName("UTF-8");
		File file;
		try {
			file = new File(URLDecoder.decode(url.getPath(), UTF8.name()));
		} catch (UnsupportedEncodingException e1) {
			throw new RuntimeException(e1);
		}
		
		if (file == null || file.isDirectory()) {
			throw new RuntimeException(
					"Cannot load a configuration");
		}

		InputStream in = null;
		PropertiesReader reader = null;
		Map<String, String> props = new HashMap<String, String>();
		try {
			in = url.openStream();
			Reader propReader = new InputStreamReader(in, UTF8);
			reader = new PropertiesReader(propReader, listDelimiter);
			
			while (reader.nextProperty()) {
				String name = reader.getPropertyName();
				String value = reader.getPropertyValue();
				props.put(name, value);
            }
			return props;

		} catch (Exception e) {
			throw new RuntimeException(
					"Unable to load the configuration from the URL " + url, e);
		} finally {
			// close the input stream
			try { 
				if (in != null) {
					in.close();
				}
				if(reader != null) {
					reader.close();
				}
			} catch (IOException e) {
				System.err.println("Could not close input stream");
			}
		}
	}
	
	/**
     * This class is used to read properties lines. These lines do
     * not terminate with new-line chars but rather when there is no
     * backslash sign a the end of the line.  This is used to
     * concatenate multiple lines for readability.
     */
    public static class PropertiesReader extends LineNumberReader
    {
        /** Stores the comment lines for the currently processed property.*/
        private List<String> commentLines;

        /** Stores the name of the last read property.*/
        private String propertyName;

        /** Stores the value of the last read property.*/
        private String propertyValue;

        /** Stores the list delimiter character.*/
        private char listDelimiter;

        /**
         * Creates a new instance of <code>PropertiesReader</code> and sets
         * the underlaying reader and the list delimiter.
         *
         * @param reader the reader
         * @param listDelimiter the list delimiter character
         * @since 1.3
         */
        public PropertiesReader(Reader reader, char listDelimiter)
        {
            super(reader);
            commentLines = new ArrayList<String>();
            this.listDelimiter = listDelimiter;
        }

        /**
         * Reads a property line. Returns null if Stream is
         * at EOF. Concatenates lines ending with "\".
         * Skips lines beginning with "#" or "!" and empty lines.
         * The return value is a property definition (<code>&lt;name&gt;</code>
         * = <code>&lt;value&gt;</code>)
         *
         * @return A string containing a property value or null
         *
         * @throws IOException in case of an I/O error
         */
        public String readProperty() throws IOException
        {
            commentLines.clear();
            StringBuffer buffer = new StringBuffer();

            while (true)
            {
                String line = readLine();
                if (line == null)
                {
                    // EOF
                    return null;
                }

                if (Utils.isCommentLine(line))
                {
                    commentLines.add(line);
                    continue;
                }

                line = line.trim();

                if (checkCombineLines(line))
                {
                    line = line.substring(0, line.length() - 1);
                    buffer.append(line);
                }
                else
                {
                    buffer.append(line);
                    break;
                }
            }
            return buffer.toString();
        }

        /**
         * Parses the next property from the input stream and stores the found
         * name and value in internal fields. These fields can be obtained using
         * the provided getter methods. The return value indicates whether EOF
         * was reached (<b>false</b>) or whether further properties are
         * available (<b>true</b>).
         *
         * @return a flag if further properties are available
         * @throws IOException if an error occurs
         * @since 1.3
         */
        public boolean nextProperty() throws IOException
        {
            String line = readProperty();

            if (line == null)
            {
                return false; // EOF
            }

            // parse the line
            String[] property = parseProperty(line);
            propertyName = property[0];
            propertyValue = Utils.unescapeJava(property[1], listDelimiter);
            return true;
        }

        /**
         * Returns the comment lines that have been read for the last property.
         *
         * @return the comment lines for the last property returned by
         * <code>readProperty()</code>
         * @since 1.3
         */
        public List<String> getCommentLines()
        {
            return commentLines;
        }

        /**
         * Returns the name of the last read property. This method can be called
         * after <code>{@link #nextProperty()}</code> was invoked and its
         * return value was <b>true</b>.
         *
         * @return the name of the last read property
         * @since 1.3
         */
        public String getPropertyName()
        {
            return propertyName;
        }

        /**
         * Returns the value of the last read property. This method can be
         * called after <code>{@link #nextProperty()}</code> was invoked and
         * its return value was <b>true</b>.
         *
         * @return the value of the last read property
         * @since 1.3
         */
        public String getPropertyValue()
        {
            return propertyValue;
        }

        /**
         * Checks if the passed in line should be combined with the following.
         * This is true, if the line ends with an odd number of backslashes.
         *
         * @param line the line
         * @return a flag if the lines should be combined
         */
        private static boolean checkCombineLines(String line)
        {
            int bsCount = 0;
            for (int idx = line.length() - 1; idx >= 0 && line.charAt(idx) == '\\'; idx--)
            {
                bsCount++;
            }

            return bsCount % 2 != 0;
        }

        /**
         * Parse a property line and return the key and the value in an array.
         *
         * @param line the line to parse
         * @return an array with the property's key and value
         * @since 1.2
         */
        private static String[] parseProperty(String line)
        {
            // sorry for this spaghetti code, please replace it as soon as
            // possible with a regexp when the Java 1.3 requirement is dropped

            String[] result = new String[2];
            StringBuffer key = new StringBuffer();
            StringBuffer value = new StringBuffer();

            // state of the automaton:
            // 0: key parsing
            // 1: antislash found while parsing the key
            // 2: separator crossing
            // 3: value parsing
            int state = 0;

            for (int pos = 0; pos < line.length(); pos++)
            {
                char c = line.charAt(pos);

                switch (state)
                {
                    case 0:
                        if (c == '\\')
                        {
                            state = 1;
                        }
                        else if (isWhiteSpace(c))
                        {
                            // switch to the separator crossing state
                            state = 2;
                        }
                        else if (isSeparator(c))
                        {
                            // switch to the value parsing state
                            state = 3;
                        }
                        else
                        {
                            key.append(c);
                        }

                        break;

                    case 1:
                        if (isWhiteSpace(c) || isSeparator(c))
                        {
                            // this is an escaped separator or white space
                            key.append(c);
                        }
                        else
                        {
                            // another escaped character, the '\' is preserved
                            key.append('\\');
                            key.append(c);
                        }

                        // return to the key parsing state
                        state = 0;

                        break;

                    case 2:
                        if (isWhiteSpace(c))
                        {
                            // do nothing, eat all white spaces
                            state = 2;
                        }
                        else if (isSeparator(c))
                        {
                            // switch to the value parsing state
                            state = 3;
                        }
                        else
                        {
                            // any other character indicates we encoutered the beginning of the value
                            value.append(c);

                            // switch to the value parsing state
                            state = 3;
                        }

                        break;

                    case 3:
                        value.append(c);
                        break;
                }
            }

            result[0] = key.toString().trim();
            result[1] = value.toString().trim();

            return result;
        }
        
        private static boolean isSeparator(char c) {
        	
        	for(int i = 0; i < SEPARATORS.length; i++) {
        		if(c == SEPARATORS[i]) {
        			return true;
        		}
        	}
        	return false;
        }
        
        private static boolean isWhiteSpace(char c) {
        	
        	for(int i = 0; i < WHITE_SPACE.length; i++) {
        		if(c == WHITE_SPACE[i]) {
        			return true;
        		}
        	}
        	return false;
        }
        
    }

}
